import socket
import threading
import os
import sys
from collections import defaultdict
import platform
import mimetypes
import time
from pathlib import Path
import hashlib  # 导入hashlib模块
from threading import Timer
  
# 文件的数据结构，包括文件名和其Hash值
dictFileType = {'fileName': '', 'fileHash': ''}
# dictFile['fileName'] = ''
# dictFile['fileHash'] = ''

# 计算指定文件的MD5


def getMD5Str(fileA, intBytes=1024):
    md5Tmp = hashlib.md5()  # 创建一个md5算法对象
    with open(fileA, 'rb') as f:  # 打开一个文件，必须是'rb'模式打开
        while 1:
            data = f.read(intBytes)  # 由于是一个文件，每次只读取固定字节
            if data:  # 当读取内容不为空时对读取内容进行update
                md5Tmp.update(data)
            else:  # 当整个文件读完之后停止update
                break
    strMD5 = md5Tmp.hexdigest()  # 获取这个文件的MD5值
    return strMD5


# 获取目录下的所有文件名，并记录文件的hash值
# 返回值listFiles为列表，元素为字典数据结构,[dict1,ditc2,...]
# dict = {'fileName':'','fileHash':''}, dict['fileName'] = '', dict['fileHash'] = ''
def GetAllBlocks(inStrFilePath):
    outListName = []
    if os.path.isdir(inStrFilePath):
        for fileA in os.listdir(inStrFilePath):
            filePathA = os.path.join(inStrFilePath, fileA)
            if not os.path.isdir(filePathA):  # 是一个文件
                fpath, strFileName = os.path.split(filePathA)
                #shotname, extension = os.path.splitext(tmpfilename)
                dictTmp = dictFileType.copy()
                dictTmp['fileName'] = strFileName
                dictTmp['fileHash'] = getMD5Str(filePathA, intBytes=1024)
                outListName.append(dictTmp)
    return outListName

# filePathDebug = r"F:\OneDrive\1-教学\区块链\4-Program\MergeV3\SwuBlockchain\block"
# GetAllBlocks(filePathDebug)


class MyException(Exception):
    pass


class Client(object):
    def __init__(self, serverhost='localhost', Version='1.10'):
        self.SERVER_HOST = serverhost
        self.SERVER_PORT = 7777
        self.Version = Version
        self.RootPath = os.path.abspath('.')  # current dir
        self.BlockDir = self.RootPath + '/block'  # block file directory
        Path(self.BlockDir).mkdir(exist_ok=True)
        print('block path:%s\n' % (self.BlockDir))

        self.UPLOAD_PORT = None
        self.shareable = True

    def start(self):
        # connect to server
        print('Connecting to the server %s:%s' %
              (self.SERVER_HOST, self.SERVER_PORT))
        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            self.server.connect((self.SERVER_HOST, self.SERVER_PORT))
        except Exception:
            print('Server Not Available.')
            return

        print('Connected')
        # upload
        uploaderProcess = threading.Thread(target=self.initUploadToPeer)
        uploaderProcess.start()
        while self.UPLOAD_PORT is None:
            # wait until upload port is initialized
            pass
        print('Listening on the upload port %s' % self.UPLOAD_PORT)

        fileListFromServer = [] #服务器上的清单
        listLocalFiles = [] #本地文件
        # main task
        while(True):
            inputStr = input('press 1: list, 2:add, 3:download,4:Empty server list,q: to exit.(%s)\n' % (time.time()))
            if inputStr=='q':
                os._exit(0)
                sys.exit(0)
            if inputStr=='1':
                # 获取服务器上的清单   
                fileListFromServer = self.getFileListFromServer()
                # [fileName,fileHash,IP,port]
                print(fileListFromServer)
            if inputStr=='2':
                # 将自己拥有的file清单，上传到server
                listLocalFiles = GetAllBlocks(self.BlockDir)
                # [dictFile]
                # dictFile:{'fileName':'','fileHash':''}
                #localHostName = socket.gethostname()
                localHostIP = socket.gethostbyname(socket.gethostname())
                localUploadPort = self.UPLOAD_PORT

                # 上传列表到server
                for dictFileA in listLocalFiles:
                    if [dictFileA['fileName'], dictFileA['fileHash'], localHostIP, localUploadPort] not in fileListFromServer:
                        # 不能有空格
                        if ' ' not in dictFileA['fileName']:
                            self.addFileToServer(dictFileA)
            if inputStr=='3':
                # 从其他client下载没有的区块
                for listB in fileListFromServer:
                    dictFileB = dictFileType.copy()
                    dictFileB['fileName'] = listB[0]
                    dictFileB['fileHash'] = listB[1]
                    if dictFileB not in listLocalFiles:
                        s1 = (socket.gethostbyname(socket.gethostname()), self.UPLOAD_PORT)
                        if((listB[2], listB[3]) != s1): #不是自己
                            self.downloadFileFromPeer(
                                listB[0], listB[1], listB[2],listB[3])  # download
            if inputStr=='4':
                # 将自己拥有的file清单，上传到server
                self.emptyServerList()

    def initUploadToPeer(self):
        # listen upload port
        self.uploader = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.uploader.bind(('', 0))
        self.UPLOAD_PORT = self.uploader.getsockname()[1]
        self.uploader.listen(5)

        while self.shareable:
            requester, addr = self.uploader.accept()
            handler = threading.Thread(
                target=self.uploadHandler, args=(requester, addr))
            handler.start()
        self.uploader.close()

    def uploadHandler(self, soc, addr):
        header = soc.recv(1024).decode().splitlines()
        try:
            version = header[0].split()[-1]
            fileName = header[0].split()[2]
            fileHash = header[0].split()[3]
            method = header[0].split()[0]
            path = '%s/%s' % (self.BlockDir, fileName)
            if version != self.Version:
                soc.sendall(str.encode(self.Version +
                                       ' 505 P2P-CI Version Not Supported\n'))
            elif not Path(path).is_file():
                soc.sendall(str.encode(self.Version + ' 404 Not Found\n'))
            elif method == 'GET':
                header = self.Version + ' 200 OK\n'
                header += 'Data: %s\n' % (time.strftime(
                    "%a, %d %b %Y %H:%M:%S GMT", time.gmtime()))
                header += 'OS: %s\n' % (platform.platform())
                header += 'Last-Modified: %s\n' % (time.strftime(
                    "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(os.path.getmtime(path))))
                header += 'Content-Length: %s\n' % (os.path.getsize(path))
                header += 'Content-Type: %s\n' % (
                    mimetypes.MimeTypes().guess_type(path)[0])
                soc.sendall(header.encode())
                # Uploading
                try:
                    print('\nUploading...')

                    send_length = 0
                    with open(path, 'r') as file:
                        to_send = file.read(1024)
                        while to_send:
                            send_length += len(to_send.encode())
                            soc.sendall(to_send.encode())
                            to_send = file.read(1024)
                except Exception:
                    raise MyException('Uploading Failed')
                print('Uploading Completed.')
            else:
                raise MyException('Bad Request.(%s)' % time.time())
        except Exception as e:
            print(e)
        except Exception:
            soc.sendall(str.encode(self.Version + '  400 P2P Bad Request\n'))
        finally:
            soc.close()

    def addFileToServer(self, dictFileA):
        # 将自己有的文件登记到server上，方便其他client访问
        msg = 'ADD block %s %s %s\n' % (
            dictFileA['fileName'], dictFileA['fileHash'], self.Version)
        msg += 'IP: %s\n' % socket.gethostbyname(socket.gethostname())
        msg += 'Port: %s\n' % self.UPLOAD_PORT
        self.server.sendall(msg.encode())
        res = self.server.recv(1024).decode()
        print('Client recieved response: \n%s' % res)

    def getFileListFromServer(self):
        try:
            l1 = 'LIST ALL %s\n' % self.Version
            l2 = 'IP: %s\n' % socket.gethostbyname(socket.gethostname())
            l3 = 'Port: %s\n' % self.UPLOAD_PORT
            msg = l1 + l2 + l3
            self.server.sendall(msg.encode())
            res = self.server.recv(1024).decode()        
            #print('Client recieved response: \n%s' % res)
            listSplit = res.strip().split('\n')
            xList = []
            if listSplit[0].split(' ')[1] == '200':#返回200, 有数据
                for i in range(1, len(listSplit)):
                    fileName = listSplit[i].split(' ')[1]
                    fileHash = listSplit[i].split(' ')[2]
                    peer_host = listSplit[i].split(' ')[3]
                    peer_port = int(listSplit[i].split(' ')[4])
                    xList.append([fileName, fileHash, peer_host, peer_port])
            return xList
        except Exception as e:
                print(e)    

    def emptyServerList(self):
        try:
            l1 = 'EMPTY %s\n' % self.Version
            l2 = 'IP: %s\n' % socket.gethostbyname(socket.gethostname())
            l3 = 'Port: %s\n' % self.UPLOAD_PORT
            msg = l1 + l2 + l3
            self.server.sendall(msg.encode())
        except Exception as e:
                print(e)   

    def downloadFileFromPeer(self, fileName, fileHash, peer_host, peer_port):
        try:
            # make connnection
            soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            # connect_ex return errors
            if soc.connect_ex((peer_host, peer_port)):
                # print('Try Local Network...')
                # if soc.connect_ex(('localhost', peer_port)):
                raise MyException('Peer Not Available')
            # make request
            msg = 'GET block %s %s %s\n' % (fileName, fileHash, self.Version)
            msg += 'Host: %s\n' % socket.gethostname()
            msg += 'OS: %s\n' % platform.platform()
            soc.sendall(msg.encode())

            # Downloading

            header = soc.recv(1024).decode()
            print('Recieve response header: \n%s' % header)
            header = header.splitlines()
            if header[0].split()[-2] == '200':
                path = '%s/%s' % (self.BlockDir, fileName)
                print('Downloading...')
                try:
                    with open(path, 'w') as file:
                        content = soc.recv(1024)
                        while content:
                            file.write(content.decode())
                            content = soc.recv(1024)
                except Exception:
                    raise MyException('Downloading Failed')

                total_length = int(header[4].split()[1])
                # print('write: %s | total: %s' % (os.path.getsize(path),
                # total_length))

                if os.path.getsize(path) < total_length:
                    raise MyException('Downloading Failed')

                print('Downloading Completed.')

            elif header[0].split()[1] == '400':
                raise MyException('Invalid Input.')
            elif header[0].split()[1] == '404':
                raise MyException('File Not Available.')
            elif header[0].split()[1] == '500':
                raise MyException('Version Not Supported.')
        finally:
            soc.close()

    def invalid_input(self):
        raise MyException('Invalid Input.')

    def shutdown(self):
        print('\nShutting Down...')
        self.server.close()
        try:
            sys.exit(0)
        except SystemExit:
            os._exit(0)


reqServerAdd = input(
    '\nInput server address to connect(leave blank = 127.0.0.1):')
if len(reqServerAdd.strip()) == 0:
    reqServerAdd = '127.0.0.1'
client = Client(serverhost=reqServerAdd)
client.start()
